/*
 * Copyright 2021 Roberto Leinardi.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.leinardi.ohos.speeddial;

import ohos.agp.animation.Animator;
import ohos.agp.animation.AnimatorProperty;
import ohos.agp.components.*;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.ShapeElement;
import ohos.agp.utils.Color;
import ohos.agp.utils.LayoutAlignment;
import ohos.app.Context;
import ohos.utils.Parcel;
import ohos.utils.Sequenceable;

import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import static com.leinardi.ohos.speeddial.SpeedDialView.ExpansionMode.BOTTOM;
import static com.leinardi.ohos.speeddial.SpeedDialView.ExpansionMode.LEFT;
import static com.leinardi.ohos.speeddial.SpeedDialView.ExpansionMode.RIGHT;
import static com.leinardi.ohos.speeddial.SpeedDialView.ExpansionMode.TOP;
import static java.lang.annotation.RetentionPolicy.SOURCE;

public class SpeedDialView extends DirectionalLayout implements Component.BindStateChangedListener {
    private static final String TAG = SpeedDialView.class.getSimpleName();
    private static final int DEFAULT_ROTATE_ANGLE = 45;
    private static final int ACTION_ANIM_DELAY = 25;
    private static final int MAIN_FAB_HORIZONTAL_MARGIN_IN_DP = 4;
    private static final int MAIN_FAB_VERTICAL_MARGIN_IN_DP = 4;
    private static final int CLICK_TIME_INTERVAL = 200;

    private final InstanceState mInstanceState = new InstanceState();
    private Context mContext;
    private final List<FabWithLabelView> mFabWithLabelViews = new ArrayList<>();
    private Element mMainFabClosedDrawable = null;
    private Element mMainFabOpenedDrawable = null;
    private Element mMainFabCloseOriginalDrawable;
    private FloatingActionButton mMainFab;
    private int mOverlayLayoutId;
    private long clickTimeMs;
    private SpeedDialOverlayLayout mOverlayLayout;
    private OnChangeListener mOnChangeListener;
    private OnActionSelectedListener mOnActionSelectedListener;

    private final OnActionSelectedListener mOnActionSelectedProxyListener = new OnActionSelectedListener() {
        @Override
        public boolean onActionSelected(SpeedDialActionItem actionItem) {
            if (mOnActionSelectedListener != null) {
                boolean consumed = mOnActionSelectedListener.onActionSelected(actionItem);
                if (!consumed) {
                    close(false);
                }
                return consumed;
            } else {
                return false;
            }
        }
    };

    public SpeedDialView(Context context) {
        super(context);
        init(context, null);
    }

    public SpeedDialView(Context context, AttrSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public SpeedDialView(Context context, AttrSet attrs, String styleName) {
        super(context, attrs, styleName);
        init(context, attrs);
    }

    public boolean getUseReverseAnimationOnClose() {
        return mInstanceState.mUseReverseAnimationOnClose;
    }

    public void setUseReverseAnimationOnClose(boolean useReverseAnimation) {
        mInstanceState.mUseReverseAnimationOnClose = useReverseAnimation;
    }

    @ExpansionMode
    public int getExpansionMode() {
        return mInstanceState.mExpansionMode;
    }

    public void setExpansionMode(@ExpansionMode int expansionMode) {
        setExpansionMode(expansionMode, false);
    }

    private void setExpansionMode(@ExpansionMode int expansionMode, boolean force) {
        if (mInstanceState.mExpansionMode != expansionMode || force) {
            mInstanceState.mExpansionMode = expansionMode;
            switch (expansionMode) {
                case TOP:
                case BOTTOM:
                    setOrientation(VERTICAL);
                    for (FabWithLabelView fabWithLabelView : mFabWithLabelViews) {
                        fabWithLabelView.setOrientation(HORIZONTAL);
                    }
                    break;
                case LEFT:
                case RIGHT:
                    setOrientation(HORIZONTAL);
                    for (FabWithLabelView fabWithLabelView : mFabWithLabelViews) {
                        fabWithLabelView.setOrientation(VERTICAL);
                    }
                    break;
            }
            close(false);
            ArrayList<SpeedDialActionItem> actionItems = getActionItems();
            clearActionItems();
            addAllActionItems(actionItems);
        }
    }

    @Override
    public void setOrientation(int orientation) {
        super.setOrientation(orientation);
    }

    public void show() {
        mMainFab.show(new FloatingActionButton.OnVisibilityChangedListener() {
            @Override
            public void onShown(FloatingActionButton fab) {
                setVisibility(VISIBLE);
            }
        });
    }

    public void hide() {
        if (isOpen()) {
            close();
        }
        mMainFab.hide(new FloatingActionButton.OnVisibilityChangedListener() {
            @Override
            public void onHidden(FloatingActionButton fab) {
                setVisibility(INVISIBLE);
            }
        });
    }

    public SpeedDialOverlayLayout getOverlayLayout() {
        return mOverlayLayout;
    }

    /**
     * Add the overlay/touch guard view to appear together with the speed dial menu.
     *
     * @param overlayLayout The view to add.
     */
    public void setOverlayLayout(SpeedDialOverlayLayout overlayLayout) {
        if (mOverlayLayout != null) {
            setClickedListener(null);
        }
        mOverlayLayout = overlayLayout;
        if (overlayLayout != null) {
            overlayLayout.setClickedListener(view -> close());
            showHideOverlay(isOpen(), false);
        }
    }

    /**
     * Appends all of the {@link SpeedDialActionItem} to the end of the list, in the order that they are returned by
     * the specified
     * collection's Iterator.
     *
     * @param actionItemCollection collection containing {@link SpeedDialActionItem} to be added to this list
     * @return a collection containing the instances of {@link FabWithLabelView} added.
     */
    public Collection<FabWithLabelView> addAllActionItems(Collection<SpeedDialActionItem> actionItemCollection) {
        ArrayList<FabWithLabelView> fabWithLabelViews = new ArrayList<>();
        for (SpeedDialActionItem speedDialActionItem : actionItemCollection) {
            fabWithLabelViews.add(addActionItem(speedDialActionItem));
        }
        return fabWithLabelViews;
    }

    /**
     * Appends the specified {@link SpeedDialActionItem} to the end of this list.
     *
     * @param speedDialActionItem {@link SpeedDialActionItem} to be appended to this list
     * @return the instance of the {@link FabWithLabelView} if the add was successful, null otherwise.
     */
    public FabWithLabelView addActionItem(SpeedDialActionItem speedDialActionItem) {
        return addActionItem(speedDialActionItem, mFabWithLabelViews.size());
    }

    /**
     * Inserts the specified {@link SpeedDialActionItem} at the specified position in this list. Shifts the element
     * currently at that position (if any) and any subsequent elements to the right (adds one to their indices).
     *
     * @param actionItem {@link SpeedDialActionItem} to be appended to this list
     * @param position   index at which the specified element is to be inserted
     * @return the instance of the {@link FabWithLabelView} if the add was successful, null otherwise.
     */
    public FabWithLabelView addActionItem(SpeedDialActionItem actionItem, int position) {
        return addActionItem(actionItem, position, true);
    }

    /**
     * Inserts the specified {@link SpeedDialActionItem} at the specified position in this list. Shifts the element
     * currently at that position (if any) and any subsequent elements to the right (adds one to their indices).
     *
     * @param actionItem {@link SpeedDialActionItem} to be appended to this list
     * @param position   index at which the specified element is to be inserted
     * @param animate    true to animate the insertion, false to insert instantly
     * @return the instance of the {@link FabWithLabelView} if the add was successful, null otherwise.
     */
    public FabWithLabelView addActionItem(SpeedDialActionItem actionItem, int position, boolean animate) {
        FabWithLabelView oldView = findFabWithLabelViewById(actionItem.getId());
        if (oldView != null) {
            return replaceActionItem(oldView.getSpeedDialActionItem(), actionItem);
        } else {
            FabWithLabelView newView = actionItem.createFabWithLabelView(mContext);
            newView.setOrientation(getOrientation() == VERTICAL ? HORIZONTAL : VERTICAL);
            newView.setOnActionSelectedListener(mOnActionSelectedProxyListener);
            int layoutPosition = getLayoutPosition(position);
            LayoutConfig layoutConfig = new LayoutConfig(LayoutConfig.MATCH_CONTENT, LayoutConfig.MATCH_CONTENT);
            layoutConfig.alignment = LayoutAlignment.VERTICAL_CENTER | LayoutAlignment.END;
            addComponent(newView, layoutPosition, layoutConfig);
            mFabWithLabelViews.add(position, newView);
            if (isOpen()) {
                if (animate) {
                    showWithAnimationFabWithLabelView(newView, 0);
                }
            } else {
                newView.setVisibility(Component.HIDE);
            }
            return newView;
        }
    }

    /**
     * Removes the {@link SpeedDialActionItem} at the specified position in this list. Shifts any subsequent elements
     * to the left (subtracts one from their indices).
     *
     * @param position the index of the {@link SpeedDialActionItem} to be removed
     * @return the {@link SpeedDialActionItem} that was removed from the list
     */
    public SpeedDialActionItem removeActionItem(int position) {
        SpeedDialActionItem speedDialActionItem = mFabWithLabelViews.get(position).getSpeedDialActionItem();
        removeActionItem(speedDialActionItem);
        return speedDialActionItem;
    }

    /**
     * Removes the specified {@link SpeedDialActionItem} from this list, if it is present. If the list does not
     * contain the element, it is unchanged.
     * <p>
     * Returns true if this list contained the specified element (or equivalently, if this list changed
     * as a result of the call).
     *
     * @param actionItem {@link SpeedDialActionItem} to be removed from this list, if present
     * @return true if this list contained the specified element
     */
    public boolean removeActionItem(SpeedDialActionItem actionItem) {
        return actionItem != null && removeActionItemById(actionItem.getId()) != null;
    }

    /**
     * Finds and removes the first {@link SpeedDialActionItem} with the given ID, if it is present. If the list does not
     * contain the element, it is unchanged.
     *
     * @param idRes the ID to search for
     * @return the {@link SpeedDialActionItem} that was removed from the list, or null otherwise
     */
    public SpeedDialActionItem removeActionItemById(int idRes) {
        return removeActionItem(findFabWithLabelViewById(idRes));
    }

    /**
     * Replace the {@link SpeedDialActionItem} at the specified position in this list with the one provided as
     * parameter.
     *
     * @param newActionItem {@link SpeedDialActionItem} to use for the replacement
     * @param position      the index of the {@link SpeedDialActionItem} to be replaced
     * @return the instance of the new {@link FabWithLabelView} if the replace was successful, null otherwise.
     */
    public FabWithLabelView replaceActionItem(SpeedDialActionItem newActionItem, int position) {
        return replaceActionItem(mFabWithLabelViews.get(position).getSpeedDialActionItem(), newActionItem);
    }

    /**
     * Replace an already added {@link SpeedDialActionItem} with the one provided as parameter.
     *
     * @param oldSpeedDialActionItem the old {@link SpeedDialActionItem} to remove
     * @param newSpeedDialActionItem the new {@link SpeedDialActionItem} to add
     * @return the instance of the new {@link FabWithLabelView} if the replace was successful, null otherwise.
     */
    public FabWithLabelView replaceActionItem(SpeedDialActionItem oldSpeedDialActionItem,
                                              SpeedDialActionItem newSpeedDialActionItem) {
        if (oldSpeedDialActionItem == null) {
            return null;
        } else {
            FabWithLabelView oldView = findFabWithLabelViewById(oldSpeedDialActionItem.getId());
            if (oldView != null) {
                int index = mFabWithLabelViews.indexOf(oldView);
                if (index < 0) {
                    return null;
                }
                removeActionItem(findFabWithLabelViewById(newSpeedDialActionItem.getId()), null, false);
                removeActionItem(findFabWithLabelViewById(oldSpeedDialActionItem.getId()), null, false);
                return addActionItem(newSpeedDialActionItem, index, false);
            } else {
                return null;
            }
        }
    }

    /**
     * Removes all of the {@link SpeedDialActionItem} from this list.
     */
    public void clearActionItems() {
        Iterator<FabWithLabelView> it = mFabWithLabelViews.iterator();
        while (it.hasNext()) {
            FabWithLabelView fabWithLabelView = it.next();
            removeActionItem(fabWithLabelView, it, true);
        }
    }

    public ArrayList<SpeedDialActionItem> getActionItems() {
        ArrayList<SpeedDialActionItem> speedDialActionItems = new ArrayList<>(mFabWithLabelViews.size());
        for (FabWithLabelView fabWithLabelView : mFabWithLabelViews) {
            speedDialActionItems.add(fabWithLabelView.getSpeedDialActionItem());
        }
        return speedDialActionItems;
    }

    /**
     * Set a listener that will be notified when a menu fab is selected.
     *
     * @param listener listener to set.
     */
    public void setOnActionSelectedListener(OnActionSelectedListener listener) {
        mOnActionSelectedListener = listener;

        for (final FabWithLabelView fabWithLabelView : mFabWithLabelViews) {
            fabWithLabelView.setOnActionSelectedListener(mOnActionSelectedProxyListener);
        }
    }

    /**
     * Set Main FloatingActionButton ClickMOnOptionFabSelectedListener.
     *
     * @param onChangeListener listener to set.
     */
    public void setOnChangeListener(final OnChangeListener onChangeListener) {
        mOnChangeListener = onChangeListener;
    }

    /**
     * Opens speed dial menu.
     */
    public void open() {
        toggle(true, true);
    }

    public void open(boolean animate) {
        toggle(true, animate);
    }

    /**
     * Closes speed dial menu.
     */
    public void close() {
        toggle(false, true);
    }

    public void close(boolean animate) {
        toggle(false, animate);
    }

    /**
     * Toggles speed dial menu.
     */
    public void toggle() {
        toggle(!isOpen(), true);
    }

    public void toggle(boolean animate) {
        toggle(!isOpen(), animate);
    }

    /**
     * Return returns true if speed dial menu is open,false otherwise.
     */
    public boolean isOpen() {
        return mInstanceState.mIsOpen;
    }

    public FloatingActionButton getMainFab() {
        return mMainFab;
    }

    public float getMainFabAnimationRotateAngle() {
        return mInstanceState.mMainFabAnimationRotateAngle;
    }

    public void setMainFabAnimationRotateAngle(float mainFabAnimationRotateAngle) {
        mInstanceState.mMainFabAnimationRotateAngle = mainFabAnimationRotateAngle;
    }

    public void setMainFabClosedDrawable(Element drawable) {
        mMainFabClosedDrawable = drawable;
        updateMainFabDrawable(false);
    }

    public void setMainFabOpenedDrawable(Element drawable) {
        mMainFabCloseOriginalDrawable = drawable;
        if (mMainFabCloseOriginalDrawable == null) {
            mMainFabOpenedDrawable = null;
        } else {
            //mMainFabOpenedDrawable = UiUtils.getRotateDrawable(mMainFabCloseOriginalDrawable, -getMainFabAnimationRotateAngle());
            mMainFabOpenedDrawable = mMainFabCloseOriginalDrawable;
        }
        updateMainFabDrawable(false);
    }

    public int getMainFabClosedBackgroundColor() {
        return mInstanceState.mMainFabClosedBackgroundColor;
    }

    public void setMainFabClosedBackgroundColor(int mainFabClosedBackgroundColor) {
        mInstanceState.mMainFabClosedBackgroundColor = mainFabClosedBackgroundColor;
        updateMainFabBackgroundColor();
    }

    public int getMainFabOpenedBackgroundColor() {
        return mInstanceState.mMainFabOpenedBackgroundColor;
    }

    public void setMainFabOpenedBackgroundColor(int mainFabOpenedBackgroundColor) {
        mInstanceState.mMainFabOpenedBackgroundColor = mainFabOpenedBackgroundColor;
        updateMainFabBackgroundColor();
    }

    public int getMainFabClosedIconColor() {
        return mInstanceState.mMainFabClosedIconColor;
    }

    public void setMainFabClosedIconColor(int mainFabClosedIconColor) {
        mInstanceState.mMainFabClosedIconColor = mainFabClosedIconColor;
        updateMainFabIconColor();
    }

    public int getMainFabOpenedIconColor() {
        return mInstanceState.mMainFabOpenedIconColor;
    }

    public void setMainFabOpenedIconColor(int mainFabOpenedIconColor) {
        mInstanceState.mMainFabOpenedIconColor = mainFabOpenedIconColor;
        updateMainFabIconColor();
    }

    @Override
    public void onComponentBoundToWindow(Component component) {
        if (mOverlayLayout == null) {
            SpeedDialOverlayLayout overlayLayout = (SpeedDialOverlayLayout) getRootView().findComponentById(mOverlayLayoutId);
            if (overlayLayout != null) {
                setOverlayLayout(overlayLayout);
            }
        }
    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {

    }

    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        getMainFab().setEnabled(enabled);
    }

    private Component getRootView() {
        Component parent = this;

        while (parent.getComponentParent() != null && parent.getComponentParent() instanceof Component) {
            parent = (Component) parent.getComponentParent();
        }

        return parent;
    }

    private int getLayoutPosition(int position) {
        if (getExpansionMode() == TOP || getExpansionMode() == LEFT) {
            return mFabWithLabelViews.size() - position;
        } else {
            return position + 1;
        }
    }

    private SpeedDialActionItem removeActionItem(FabWithLabelView view, Iterator<FabWithLabelView> it, boolean animate) {
        if (view != null) {
            SpeedDialActionItem speedDialActionItem = view.getSpeedDialActionItem();
            if (it != null) {
                it.remove();
            } else {
                mFabWithLabelViews.remove(view);
            }

            if (isOpen()) {
                if (mFabWithLabelViews.isEmpty()) {
                    close();
                }
                if (animate) {
                    UiUtils.shrinkAnim(view, true);
                } else {
                    removeComponent(view);
                }
            } else {
                removeComponent(view);
            }
            return speedDialActionItem;
        } else {
            return null;
        }
    }

    private SpeedDialActionItem removeActionItem(FabWithLabelView view) {
        return removeActionItem(view, null, true);
    }

    private void init(Context context, AttrSet attrs) {
        mContext = context;
        mMainFab = createMainFab();
        addComponent(mMainFab);
        setClipEnabled(false);
        setBindStateChangedListener(this);
        try {
            setEnabled(AttrUtils.getBooleanValueByAttr(attrs, "ohos_enabled", isEnabled()));
            setUseReverseAnimationOnClose(AttrUtils.getBooleanValueByAttr(attrs, "sdUseReverseAnimationOnClose", getUseReverseAnimationOnClose()));
            setMainFabAnimationRotateAngle(AttrUtils.getFloatValueByAttr(attrs, "sdMainFabAnimationRotateAngle", getMainFabAnimationRotateAngle()));
            Element openElement = AttrUtils.getElementValueByAttr(attrs, "sdMainFabClosedSrc", new ShapeElement());
            setMainFabClosedDrawable(openElement);

            Element closeElement = AttrUtils.getElementValueByAttr(attrs, "sdMainFabOpenedSrc", new ShapeElement());
            setMainFabOpenedDrawable(closeElement);

            setExpansionMode(AttrUtils.getIntValueByAttr(attrs, "sdExpansionMode", getExpansionMode()), true);

            setMainFabClosedBackgroundColor(AttrUtils.getColorValueByAttr(attrs, "sdMainFabClosedBackgroundColor",
                    new Color(getMainFabClosedBackgroundColor())).getValue());
            setMainFabOpenedBackgroundColor(AttrUtils.getColorValueByAttr(attrs, "sdMainFabOpenedBackgroundColor",
                    new Color(getMainFabOpenedBackgroundColor())).getValue());
            setMainFabClosedIconColor(AttrUtils.getColorValueByAttr(attrs, "sdMainFabClosedIconColor",
                    new Color(getMainFabClosedIconColor())).getValue());
            setMainFabOpenedIconColor(AttrUtils.getColorValueByAttr(attrs, "sdMainFabOpenedIconColor",
                    new Color(getMainFabOpenedIconColor())).getValue());
            mOverlayLayoutId = AttrUtils.getIntValueByAttr(attrs, "sdOverlayLayout", SpeedDialActionItem.RESOURCE_NOT_SET);
        } catch (Exception e) {
            LogUtil.debug(TAG, "Failure setting SpeedDialView attrs:" + e.getMessage());
        }
    }

    private FloatingActionButton createMainFab() {
        FloatingActionButton floatingActionButton = new FloatingActionButton(getContext());
        DirectionalLayout.LayoutConfig layoutParams = new DirectionalLayout.LayoutConfig(ComponentContainer.LayoutConfig.MATCH_CONTENT,
                ComponentContainer.LayoutConfig.MATCH_CONTENT);
        layoutParams.alignment = LayoutAlignment.END;
        int marginHorizontal = UiUtils.dpToPx(getContext(), MAIN_FAB_HORIZONTAL_MARGIN_IN_DP);
        int marginVertical = UiUtils.dpToPx(getContext(), MAIN_FAB_VERTICAL_MARGIN_IN_DP);
        layoutParams.setMargins(marginHorizontal, marginVertical, marginHorizontal, marginVertical);
        floatingActionButton.setId(ResourceTable.Integer_sd_main_fab);
        floatingActionButton.setLayoutConfig(layoutParams);
        floatingActionButton.setClickable(true);
        floatingActionButton.setFocusable(FOCUS_ENABLE);
        floatingActionButton.setShowShadow(true);
        floatingActionButton.setButtonSize(FloatingActionButton.SIZE_NORMAL);
        floatingActionButton.setClickedListener(component -> {
            if (System.currentTimeMillis() - clickTimeMs <= CLICK_TIME_INTERVAL) {
                return;
            }
            clickTimeMs = System.currentTimeMillis();
            if (isOpen()) {
                if (mOnChangeListener == null || !mOnChangeListener.onMainActionSelected()) {
                    close();
                }
            } else {
                open();
            }
        });
        return floatingActionButton;
    }

    private void toggle(boolean show, boolean animate) {
        if (show && mFabWithLabelViews.isEmpty()) {
            show = false;
            if (mOnChangeListener != null) {
                mOnChangeListener.onMainActionSelected();
            }
        }
        if (isOpen() == show) {
            return;
        }
        mInstanceState.mIsOpen = show;
        visibilitySetup(show, animate, mInstanceState.mUseReverseAnimationOnClose);
        updateMainFabDrawable(animate);
        updateMainFabBackgroundColor();
        updateMainFabIconColor();
        showHideOverlay(show, animate);
        if (mOnChangeListener != null) {
            mOnChangeListener.onToggleChanged(show);
        }
    }

    private void updateMainFabDrawable(boolean animate) {
        if (isOpen()) {
            if (mMainFabOpenedDrawable != null) {
                mMainFab.setImageElement(mMainFabOpenedDrawable);
            }
            UiUtils.rotateForward(mMainFab, getMainFabAnimationRotateAngle(), animate);
        } else {
            UiUtils.rotateBackward(mMainFab, getMainFabAnimationRotateAngle(), animate);
            mMainFab.setImageElement(mMainFabClosedDrawable);
        }
    }

    private void updateMainFabBackgroundColor() {
        int color;
        if (isOpen()) {
            color = getMainFabOpenedBackgroundColor();
        } else {
            color = getMainFabClosedBackgroundColor();
        }
        if (color != SpeedDialActionItem.RESOURCE_NOT_SET) {
            mMainFab.setColorNormal(color);
        } else {
            mMainFab.setColorNormal(UiUtils.getAccentColor(getContext()));
        }
    }

    private void updateMainFabIconColor() {
        int color;
        if (isOpen()) {
            color = getMainFabOpenedIconColor();
        } else {
            color = getMainFabClosedIconColor();
        }
        if (color != SpeedDialActionItem.RESOURCE_NOT_SET) {
            mMainFab.setColorNormal(color);
        }
    }

    private void showHideOverlay(boolean show, boolean animate) {
        if (mOverlayLayout != null) {
            if (show) {
                mOverlayLayout.show(animate);
            } else {
                mOverlayLayout.hide(animate);
            }
        }
    }

    private FabWithLabelView findFabWithLabelViewById(int id) {
        for (FabWithLabelView fabWithLabelView : mFabWithLabelViews) {
            if (fabWithLabelView.getId() == id) {
                return fabWithLabelView;
            }
        }
        return null;
    }

    /**
     * Set menus visibility (visible or invisible).
     */
    private void visibilitySetup(boolean visible, boolean animate, boolean reverseAnimation) {
        int size = mFabWithLabelViews.size();
        if (visible) {
            for (int i = 0; i < size; i++) {
                FabWithLabelView fabWithLabelView = mFabWithLabelViews.get(i);
                fabWithLabelView.setAlpha(1);
                fabWithLabelView.setVisibility(Component.VISIBLE);
                if (animate) {
                    showWithAnimationFabWithLabelView(fabWithLabelView, i * ACTION_ANIM_DELAY);
                }
                if (i == 0) {
                    fabWithLabelView.getFab().requestFocus();
                }
            }
        } else {
            for (int i = 0; i < size; i++) {
                int index = reverseAnimation ? size - 1 - i : i;
                FabWithLabelView fabWithLabelView = mFabWithLabelViews.get(index);
                if (animate) {
                    if (reverseAnimation) {
                        hideWithAnimationFabWithLabelView(fabWithLabelView, i * ACTION_ANIM_DELAY);
                    } else {
                        UiUtils.shrinkAnim(fabWithLabelView, false);
                    }
                } else {
                    fabWithLabelView.setAlpha(0);
                    fabWithLabelView.setVisibility(Component.HIDE);
                }
            }
        }
    }

    private void showWithAnimationFabWithLabelView(FabWithLabelView fabWithLabelView, int delay) {
        UiUtils.enlargeAnim(fabWithLabelView.getFab(), delay);
        if (fabWithLabelView.isLabelEnabled()) {
            StackLayout labelBackground = fabWithLabelView.getLabelBackground();

            AnimatorProperty animatorProperty = new AnimatorProperty(labelBackground);
            animatorProperty.setDuration(150);
            animatorProperty.setDelay(delay);
            animatorProperty.alphaFrom(0f).alpha(1f);
            animatorProperty.start();
        }
    }

    private void hideWithAnimationFabWithLabelView(final FabWithLabelView fabWithLabelView, int delay) {
        UiUtils.shrinkAnim(fabWithLabelView.getFab(), delay);
        if (fabWithLabelView.isLabelEnabled()) {
            final StackLayout labelBackground = fabWithLabelView.getLabelBackground();

            AnimatorProperty animatorProperty = new AnimatorProperty(labelBackground);
            animatorProperty.setDuration(150);
            animatorProperty.setDelay(delay);
            animatorProperty.alphaFrom(1.0f).alpha(0f);
            animatorProperty.setStateChangedListener(new AnimStateChangedListener() {
                @Override
                public void onEnd(Animator animator) {
                    labelBackground.setVisibility(Component.HIDE);
                }
            });
            animatorProperty.start();
        }
    }

    /**
     * Listener for handling events on option fab's.
     */
    public interface OnChangeListener {
        /**
         * Called when the main action has been clicked.
         *
         * @return true to keep the Speed Dial open, false otherwise.
         */
        boolean onMainActionSelected();

        /**
         * Called when the toggle state of the speed dial menu changes (eg. it is opened or closed).
         *
         * @param isOpen true if the speed dial is open, false otherwise.
         */
        void onToggleChanged(boolean isOpen);
    }

    /**
     * Listener for handling events on option fab's.
     */
    public interface OnActionSelectedListener {
        /**
         * Called when a speed dial action has been clicked.
         *
         * @param actionItem the {@link SpeedDialActionItem} that was selected.
         * @return true to keep the Speed Dial open, false otherwise.
         */
        boolean onActionSelected(SpeedDialActionItem actionItem);
    }

    @Retention(SOURCE)
    public @interface ExpansionMode {
        int TOP = 0;
        int BOTTOM = 1;
        int LEFT = 2;
        int RIGHT = 3;
    }

    private static class InstanceState implements Sequenceable {
        private boolean mIsOpen = false;
        private int mMainFabClosedBackgroundColor = SpeedDialActionItem.RESOURCE_NOT_SET;
        private int mMainFabOpenedBackgroundColor = SpeedDialActionItem.RESOURCE_NOT_SET;
        private int mMainFabClosedIconColor = SpeedDialActionItem.RESOURCE_NOT_SET;
        private int mMainFabOpenedIconColor = SpeedDialActionItem.RESOURCE_NOT_SET;
        @ExpansionMode
        private int mExpansionMode = TOP;
        private float mMainFabAnimationRotateAngle = DEFAULT_ROTATE_ANGLE;
        private boolean mUseReverseAnimationOnClose = false;
        private ArrayList<SpeedDialActionItem> mSpeedDialActionItems = new ArrayList<>();

        @Override
        public boolean marshalling(Parcel dest) {
            dest.writeByte(this.mIsOpen ? (byte) 1 : (byte) 0);
            dest.writeInt(this.mMainFabClosedBackgroundColor);
            dest.writeInt(this.mMainFabOpenedBackgroundColor);
            dest.writeInt(this.mMainFabClosedIconColor);
            dest.writeInt(this.mMainFabOpenedIconColor);
            dest.writeInt(this.mExpansionMode);
            dest.writeFloat(this.mMainFabAnimationRotateAngle);
            dest.writeByte(this.mUseReverseAnimationOnClose ? (byte) 1 : (byte) 0);
            dest.writeSequenceableList(this.mSpeedDialActionItems);
            return true;
        }

        @Override
        public boolean unmarshalling(Parcel in) {
            this.mIsOpen = in.readByte() != 0;
            this.mMainFabClosedBackgroundColor = in.readInt();
            this.mMainFabOpenedBackgroundColor = in.readInt();
            this.mMainFabClosedIconColor = in.readInt();
            this.mMainFabOpenedIconColor = in.readInt();
            this.mExpansionMode = in.readInt();
            this.mMainFabAnimationRotateAngle = in.readFloat();
            this.mUseReverseAnimationOnClose = in.readByte() != 0;
            this.mSpeedDialActionItems = in.createSequenceable(ClassLoader.getSystemClassLoader());
            return true;
        }

        @Override
        public boolean hasFileDescriptor() {
            return false;
        }

        public InstanceState() {
        }
    }

}
